package top.mcwebsite.lib.router.processor

import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.KotlinPoetKspPreview
import com.squareup.kotlinpoet.ksp.writeTo
import java.util.Locale
import kotlin.math.abs
import top.mcwebsite.lib.router.annotation.ParamType
import top.mcwebsite.lib.router.processor.utils.rawType


object RouteNavGenerate {

    const val PACKAGE = "top.mcwebsite.lib.route"
    const val ROUTE_FILE_NAME_PREFIX = "RouteBuilder"

    private val NavGraphBuilderClass = ClassName("androidx.navigation", "NavGraphBuilder")
    private val composable = MemberName("androidx.navigation.compose", "composable", true)
    private val navArgument = MemberName("androidx.navigation", "navArgument")
    private val NavTypeClass = ClassName("androidx.navigation", "NavType")

    private fun getFileName(routeMeta: List<RouteMeta>): String {
        return ROUTE_FILE_NAME_PREFIX + abs(routeMeta.first().routePath.hashCode())
    }

    @OptIn(KotlinPoetKspPreview::class)
    fun List<RouteMeta>.generate(codeGenerator: CodeGenerator, logger: KSPLogger) {
        val fileName = getFileName(this)
        FileSpec.builder(PACKAGE, fileName)
            .addFunction(
                FunSpec.builder(fileName.replaceFirstChar { it.lowercase(Locale.ROOT) })
                    .receiver(NavGraphBuilderClass)
                    .generateRoutes(this)
                    .build()
            )
            .build()
            .writeTo(codeGenerator, Dependencies.ALL_FILES)
    }

    private fun FunSpec.Builder.generateRoutes(routes: List<RouteMeta>): FunSpec.Builder {
        routes.forEach {
            generateRoute(it)
        }
        return this
    }

    private fun FunSpec.Builder.generateRoute(route: RouteMeta): FunSpec.Builder {
        val codeBlock = CodeBlock.builder()
        codeBlock.add("%M", composable)
        if (route.params.isEmpty()) {
            codeBlock.add("(%S) {${System.lineSeparator()}", route.routePath)
        } else {
            codeBlock.add("(${System.lineSeparator()}")
            codeBlock.indent()
            codeBlock.addStatement("route = %S,", route.routePath)
            codeBlock.addStatement("arguments = listOf(")
            codeBlock.indent()
            route.params
                .filter { it.type.isPrimitiveType || it.type == ParamType.STRING }
                .forEach {
                    // 只注册基本类型 ?
                    codeBlock.addStatement("%M(%S) {", navArgument, it.name)
                    codeBlock.indent()
                    codeBlock.addStatement("type = %T.%L", NavTypeClass, it.type.paramTypeToNavType())
                    codeBlock.unindent()
                    codeBlock.addStatement("},")
                }
            codeBlock.unindent()
            codeBlock.addStatement(")")
            codeBlock.unindent()
            codeBlock.add(") { backStackEntry ->${System.lineSeparator()}")
        }
        codeBlock.indent()
        codeBlock.add(route.functionName + "(")
        if (route.params.isEmpty()) {
            codeBlock.addStatement(")")
        } else {
            codeBlock.add(System.lineSeparator())
            codeBlock.indent()
            route.params.forEach {
                codeBlock.add("%L = %L", it.funParamName, generateGetParam(it))
                if (!it.type.isPrimitiveType && !it.nullable) {
                    codeBlock.addStatement(
                        " ?: throw %T(%S),",
                        IllegalArgumentException::class.asClassName(),
                        "${it.name} must not be null"
                    )
                } else {
                    codeBlock.add("," + System.lineSeparator())
                }
            }
            codeBlock.unindent()
            codeBlock.addStatement(")")
        }
        codeBlock.unindent()
        codeBlock.addStatement("}")
        addCode(codeBlock.build())
        return this
    }

    private fun generateGetParam(paramMeta: ParamMeta): CodeBlock {
        return CodeBlock.builder()
            .add("backStackEntry.arguments!!.%L", generateGetArgumentWithParamType(paramMeta))
            .build()
    }

    private fun ParamType.paramTypeToNavType(): String = when (this) {
        ParamType.BYTE -> "ByteType"
        ParamType.SHORT -> "ShortType"
        ParamType.INT -> "IntType"
        ParamType.LONG -> "LongType"
        ParamType.STRING -> "StringType"
        ParamType.BOOLEAN -> "BooleanType"
        ParamType.CHAR -> "CharType"
        ParamType.FLOAT -> "FloatType"
        ParamType.DOUBLE -> "DoubleType"
        else -> ""
    }

    private fun generateGetArgumentWithParamType(paramMeta: ParamMeta): CodeBlock {
        return when (paramMeta.type) {
            ParamType.INT -> CodeBlock.of("getInt(%S)", paramMeta.name)
            ParamType.BYTE -> CodeBlock.of("getByte(%S)", paramMeta.name)
            ParamType.SHORT -> CodeBlock.of("getShort(%S)", paramMeta.name)
            ParamType.LONG -> CodeBlock.of("getLong(%S)", paramMeta.name)
            ParamType.STRING -> CodeBlock.of("getString(%S)", paramMeta.name)
            ParamType.BOOLEAN -> CodeBlock.of("getBoolean(%S)", paramMeta.name)
            ParamType.CHAR -> CodeBlock.of("getChar(%S)", paramMeta.name)
            ParamType.FLOAT -> CodeBlock.of("getFloat(%S)", paramMeta.name)
            ParamType.DOUBLE -> CodeBlock.of("getDouble(%S)", paramMeta.name)
            ParamType.INT_ARRAY -> CodeBlock.of("getIntArray(%S)", paramMeta.name)
            ParamType.BYTE_ARRAY -> CodeBlock.of("getByteArray(%S)", paramMeta.name)
            ParamType.SHORT_ARRAY -> CodeBlock.of("getShortArray(%S)", paramMeta.name)
            ParamType.LONG_ARRAY -> CodeBlock.of("getLongArray(%S)", paramMeta.name)
            ParamType.STRING_ARRAY -> CodeBlock.of("getStringArray(%S)", paramMeta.name)
            ParamType.BOOLEAN_ARRAY -> CodeBlock.of("getBooleanArray(%S)", paramMeta.name)
            ParamType.CHAR_ARRAY -> CodeBlock.of("getCharArray(%S)", paramMeta.name)
            ParamType.FLOAT_ARRAY -> CodeBlock.of("getFloatArray(%S)", paramMeta.name)
            ParamType.DOUBLE_ARRAY -> CodeBlock.of("getDoubleArray(%S)", paramMeta.name)
            ParamType.PARCELABLE -> CodeBlock.builder()
                .add(
                    "getParcelable(%S, %T::class.java)",
                    paramMeta.name,
                    paramMeta.typeName.rawType.copy(nullable = false)
                )
                .apply {
                    if (paramMeta.typeName !is ClassName) {
                        add(" as %T", paramMeta.typeName.copy(true))
                    }
                }
                .build()

            ParamType.SERIALIZATION ->  CodeBlock.builder()
                .add(
                    "getSerializable(%S, %T::class.java)",
                    paramMeta.name,
                    paramMeta.typeName.rawType.copy(nullable = false)
                )
                .apply {
                    if (paramMeta.typeName !is ClassName) {
                        add(" as %T", paramMeta.typeName.copy(true))
                    }
                }
                .build()

            else -> {
                CodeBlock.builder().build()
            }
        }
    }


}